Amazon SESの送信ログをS3バケットに保存する設定をCloudFormationでサクッと作る
初めに
Amazon SESでメールを送信されている皆様、送信されたメールのログはとっていますか?
Amazon SESでは特に何も設定しなくてもログが取られる...ということはなく自分で意図してログを設定しないと送信したメールのログは取られません。
そのため特に何も意識せずとりあえず使ってみた、としているとバウンスレートが上がってきた場合や未着問い合わせ時に調査ができず手詰まりとなってしまう場合もあります。
ログの設定くらい数ページぽちぽちすればできるでしょう。と思いたいところですがいくつかのサービスを組み合わせる必要がありIAMロールの設定等地味に手間です。
シンプルなS3バケットへのログ保管設定も意外と当ブログになかったので、CloudFormationでサクッと作れるテンプレートを作ってみます。
構成図
今回はSESはドメイン単位で検証しているものとして執筆いたしますがメールアドレス単位で認証を行っても同じように利用は可能です。
SESのドメイン検証も行ってしまいたいところですが、残念ながら標準ではCloudFormationはドメイン認証未対応なので含めていません(メール認証は対応)。
一応カスタムリソースを使うことでドメイン認証もできるのですがサクッと行かなくなってしまうので利用はしません。
また、その設定については今回の記事では特に触れませんのでご了承ください。
想定している環境像
メール未着の確認やバウンスレート上昇時など困った時のトラブルシュート程度に後で確認できれば良く、メール送信量もあまり多くない(一日数千通程)場合を想定します。
サクッと最低限の設定値でとりあえず最低限のログが取りたいを前提にしていますし、また大規模で頻繁に検索のであればOpenSearch等別の格納先を利用した方が良いでしょう。
作成されるもの
- 配信成功、バウンス、拒否、苦情を対象としてログを保管する変更セット
- 上記のイベントを保管するためのS3とそこまで配達するFirehoseを作成し結びつける
- Firehose自体のエラーを出力するCloudWatch Logsの設定
- (オプション)バウンス、苦情時に個別に通知をメールで受け取るためのSNSのトピックとサブスクリプション
バウンス・苦情通知は高まるとSESの利用停止に繋がるので、普段からどの程度起こるのかリアルタイムで見られるように設定しておくのが好ましい場合もあるのでついでに作っておきます(S3から探さなくてもサクッと見れる意味でも)。
ただ発生頻度によってはものすごい量が通知されてうまくフィルタしないとメールボックスが埋められることもあるのでオプション扱いです。
作成しないもの
特に意識して欲しいものだけです。
- ドメイン・アドレス自体の認証とそこへの設定の割り当て
- 前述の通りCloudFormationがドメイン認証が現状対応していないため
- メールの開封クリック等の直接未着に影響の出ないイベントの記録
- 苦情については配信停止の原因になるので例外的に作成している
- レンダリング失敗は送信以前の問題のため作成しない側に含む
- ストレージクラスの変更をするライフサイクル
- ログサイズが極小の場合コストが高まる可能性がある
- バウンス・苦情率に基づくCloudWatch Alarm
- アカウント単位の設定になるため今回は含めない
テンプレート
本テンプレートはID(ドメイン)の作成や認証とは独立しているためスタック作成タイミングに縛りはありません。
DNSレコードの検証待ちにスタックを作成するのも良いのではないでしょうか。
AWSTemplateFormatVersion: 2010-09-09 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: General Parameters: - NoColonDomain - LogExpirationInDays - Label: default: Amazon SNS Parameters: - BaounceFeedbackAddress - ComplaintFeedbackAddress Parameters: NoColonDomain: Description: The domain name without the "." ex) example.com -> examplecom Type: String LogExpirationInDays: Description: Log expiration (day) Type: Number BaounceFeedbackAddress: Description: E-mail address for bounce feedback. empty if not needed Type: String ComplaintFeedbackAddress: Description: E-mail address for complaint feedback. empty if not needed Type: String Conditions: ExistBounceFeedbackAddress: !Not [!Equals ["", !Ref BaounceFeedbackAddress]] ExistComplaintFeedbackAddress: !Not [!Equals ["", !Ref ComplaintFeedbackAddress]] Resources: #---------------------- #--- SES Configuration #---------------------- ConfigurationSet: Type: AWS::SES::ConfigurationSet Properties: Name: !Sub ${NoColonDomain}-configuration DeliveryOptions: TlsPolicy: REQUIRE MailLogDeliverEvent: Type: AWS::SES::ConfigurationSetEventDestination Properties: ConfigurationSetName: !Ref ConfigurationSet EventDestination: Name: !Sub ${NoColonDomain}-ses-log-deliver Enabled: True MatchingEventTypes: - delivery - reject - bounce - complaint KinesisFirehoseDestination: DeliveryStreamARN: !GetAtt [SESLogDeliver, Arn] IAMRoleARN: !GetAtt [SESMailDeliverRole, Arn] SESMailDeliverRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${NoColonDomain}-ses-role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: Action: sts:AssumeRole Effect: Allow Principal: Service: ses.amazonaws.com Path: / Policies: - PolicyName: !Sub ${NoColonDomain}-ses-policy PolicyDocument: Version: 2012-10-17 Statement: Effect: Allow Action: - firehose:PutRecordBatch Resource: !GetAtt [SESLogDeliver, Arn] #---------------------- #--- S3 #---------------------- MailLogBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${NoColonDomain}-mail-log PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True BucketEncryption: ServerSideEncryptionConfiguration: - BucketKeyEnabled: True ServerSideEncryptionByDefault: SSEAlgorithm: AES256 LifecycleConfiguration: Rules: - Id: !Sub Delete-After-${LogExpirationInDays}Days ExpirationInDays: !Ref LogExpirationInDays Status: Enabled #---------------------- #--- Firehose #---------------------- SESLogDeliver: Type: AWS::KinesisFirehose::DeliveryStream Properties: DeliveryStreamName: !Sub ${NoColonDomain}-ses-log-deliver DeliveryStreamType: DirectPut ExtendedS3DestinationConfiguration: BucketARN: !GetAtt [MailLogBucket, Arn] BufferingHints: IntervalInSeconds: 900 SizeInMBs: 128 CloudWatchLoggingOptions: Enabled: True #NOTE: LogGroup側からRefで撮りたいが${SESLogDeliver}がLogGroup側で欲しく循環参照がかかるのでベタ書きする LogGroupName: !Sub /aws/kinesisfirehose/${NoColonDomain}-ses-log-deliver LogStreamName: S3Delivery CompressionFormat: GZIP ErrorOutputPrefix: "!{firehose:error-output-type}/" RoleARN: !GetAtt [FirehoseRole, Arn] FirehoseCWLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/kinesisfirehose/${SESLogDeliver} RetentionInDays: !Ref LogExpirationInDays FirehoseCWLogStream: Type: AWS::Logs::LogStream Properties: LogGroupName: !Ref FirehoseCWLogGroup LogStreamName: S3Delivery FirehoseRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${NoColonDomain}-firehose-role Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: firehose.amazonaws.com Policies: - PolicyName: !Sub ${NoColonDomain}-firehose-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 's3:PutObject' Resource: !Sub ${MailLogBucket.Arn}/* #---------------------- #--- SNS #---------------------- BounceFeedbackTopic: Condition: ExistBounceFeedbackAddress Type: AWS::SNS::Topic Properties: TopicName: !Sub ${NoColonDomain}-bounce-feedback-topic DisplayName: !Sub ${NoColonDomain}-bounce-feedback-topic BounceFeedbackSubscription: Condition: ExistBounceFeedbackAddress Type: AWS::SNS::Subscription Properties: TopicArn: !Ref BounceFeedbackTopic Protocol: email Endpoint: !Ref BaounceFeedbackAddress ComplaintFeedbackTopic: Condition: ExistComplaintFeedbackAddress Type: AWS::SNS::Topic Properties: TopicName: !Sub ${NoColonDomain}-complaint-feedback-topic DisplayName: !Sub ${NoColonDomain}-complaint-feedback-topic ComplaintFeedbackSubscription: Condition: ExistComplaintFeedbackAddress Type: AWS::SNS::Subscription Properties: TopicArn: !Ref ComplaintFeedbackTopic Protocol: email Endpoint: !Ref ComplaintFeedbackAddress
SNSによる通知先アドレスを設定している場合対象アドレスに確認のメールが届くのでリンク内のアドレスをクリックして検証を済ませておいてください。
入力値
- NoColonDomain
- SESで利用するホスト名からコロンを抜いたもの
- テンプレート内の視認性確保のために抜きで入力する方式を採用
- プレフィックスに使ってるだけなので区別できる英数字なら基本大丈夫です
- SESで利用するホスト名からコロンを抜いたもの
- LogExpirationInDays
- ログの保存期間(日単位)
- BaounceFeedbackAddress(必要な場合のみ)
- バウンス発生時の通知先アドレス
- ComplaintFeedbackAddress(必要な場合のみ)
- 苦情発生時の通知先アドレス
作成された設定の割り当て
作成やドメインの認証についてはここでは割愛しますので別途済ませておいてください。
マネジメントコンソール内の左メニュー「検証済み ID」を開き割り当てたいドメインを選択し詳細より以下の設定を行います。
変更セットの割り当て
「変更セット」のタブから「編集」を開きます。
「デフォルト設定セットの割り当て」にチェックを入れ、作成された変更セット({{NoColonDomain}}-configuration
)を割り当て保存します。
メール通知の割り当て
「通知」のタブから「編集」を開きます。
現時点の仕様ですとデフォルトで「フィードバック転送」は有効なので設定は不要ですが、もし無効になっている場合は有効化してください。
バウンス・苦情フィードバックに作成されたトピック({{NoColonDomain}}-{{bounce or cmplaint}}-feedback-topic}}
)を割り当て保存します。
通知時点で詳細な情報が欲しい場合は「元のEメールヘッダを含める」にチェックを入れておきましょう。
終わりに
以上で設定は終わりです。
入力を減らすために決め打ちの部分も多いものですが、個人的には小規模なシステムで偶のメール未着等の問い合わせやバウンス・苦情が上がり止められそうになった場合に調査、くらいの感じであればこれでも十分かなとは思っています。
ログ出したいけどどのようにすればいいかいまいちイメージつかなって人が一度実物見てイメージを掴むために使ってもいいでしょう。
できる限りログファイルがまとまるようにFirehoseのバッファを最大限まで伸ばしていますが、とは言え15分or128MBで1ファイルとなるのでどうしても塊が小さく横断検索はやや手間なのでアプリ等送信元から時間をある程度推定するかAthenaを使う形にはなります。
調査の意味ではSESやFirehoseの指定先としてCloudWatch Logsがあれば良いのですが現時点では直接の利用ができなさそうなのが残念です。
Amazon SES単体で送信履歴が取れるようになりました(2023/09/08追記)
2023/08/31のアップデートにてAmazon SESのVirtual Delivery Managerに機能追加が追加され複数のサービスを組み合わせることなくAmazon SESで送信履歴が取れるようになりました。
こちらは最長30日までであることと今回の方法ほど詳細な情報が取れるものではありませんが、日常的な調査の場合はこちらの方が便利な可能性がありますのでご一読いただければ幸いです。